文件操作

文件操作

open - 重要

我们首先需要创建一个文件对象,借由这个对象实现对文件的读写。

open() 方法用于打开一个文件,并返回文件对象。在对文件进行处理过程都需要使用到这个函数,如果该文件无法被打开,会抛出 OSError。

open(file, mode='r', buffering=None, encoding=None, errors=None, newline=None, closefd=True)

参数说明:

常用参数就三个:

buffering 有时候也会手动设置一下

现在重点说一下 mode 参数,

模式的值,只能 create(对应 x)、read(对应 r)、write(对应 w)、append(对应 a)中的一种,然后再搭配 b(二进制模式)、+(增加读或者写功能),不能随意组合出 rw 这种模式。

默认为 rt,也就是读文本模式。

模式 描述
t 文本模式 (默认)。
x 写模式,新建一个文件,如果该文件已存在则会报错
b 二进制模式。
+ 打开一个文件进行更新 (可读可写)。
U 通用换行模式(Python 3 不支持)。
r 以只读方式打开文件。文件的指针将会放在文件的开头。这是默认模式。如果文件不存在则会报错
rb 以二进制格式打开一个文件用于只读。文件指针将会放在文件的开头。这是默认模式。一般用于非文本文件如图片等
r+ 打开一个文件用于读写。文件指针将会放在文件的开头。可用于边读边改的场景
rb+ 以二进制格式打开一个文件用于读写。文件指针将会放在文件的开头。一般用于非文本文件如图片等。
w 打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件
wb 以二进制格式打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。一般用于非文本文件如图片等。
w+ 打开一个文件用于读写。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。
wb+ 以二进制格式打开一个文件用于读写。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。一般用于非文本文件如图片等。
a 打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。
ab 以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。
a+ 打开一个文件用于读写。如果该文件已存在,文件指针将会放在文件的结尾。文件打开时会是追加模式。如果该文件不存在,创建新文件用于读写。
ab+ 以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。如果该文件不存在,创建新文件用于读写。

简单总结,横向对比如下:

模式 r r+ w w+ a a+
+ + + +
+ + + + +
创建 + + + +
覆盖 + +
指针在开始 + + + +
指针在结尾 + +

返回结果:

Open() 返回一个文件对象,其类型取决于 mode 参数,并且通过这些类型的文件对象来实现标准文件操作,比如的读写。

close

注意:使用 open() 方法一定要保证关闭文件对象,即调用 close() 方法

我们用 Python 的 open 方法打开一个文件之后,会占用这个文件,此时,其他的工具无法删除这个文件,也无法重命名这个文件,(但是是可以看到和更新这个文件的),因此,我们使用完一个文件之后,一定要记得 close。解除占用。

在 Windows 上,创建了文件对象之后,读取文件内容之前,通过别的工具修改文件的内容,python 的文件对象是可以看到这个更新的内容的

同时,close() 方法在调用的时候,也会把保留在缓存中的内容写入到文件中。然后再关闭文件对象

一个好的习惯是,创建了文件对象之后,立即写一个关闭方法,这样,就不容易忘掉调用关闭方法

读操作

open 方法的 mode 选择以 r 开头的即可

文件指针

我们在读取文件的时候,有一个文件指针标识我们当前在哪里,所有的读取操作都是以指针所在位置为相对位置进行操作,读操作会默认向后移动这个文件指针,多次调用读操作的时候,后面的读操作使用的是前面的读操作移动之后的文件指针。

我们可以通过 tell 方法知道当前指针所处的位置,以及可以通过 seek 方法随意修改指针的位置,实现从任意位置开始读文件。

文件指针默认指向文件的第一个字符(文本模式)或者字节(二进制模式)。

常用方法

读文件:

其实读文件问了避免频繁访问系统磁盘,也是有缓冲的,对缓冲区的配置,请看 open 小节的 buffering 参数

读缓冲演示起来不直观,这里我们就不写代码演示了

查看和移动文件当前的指针所在的位置:

实践

遍历结果

我们可以先 readlines 读出所有的行,然后 for 循环遍历这个列表,也可以直接 for 循环遍历文件对象

# 文件可以为绝对路径,也可以为相对路径
# file_path = "C:/Users/wwwli/Desktop/test_file.txt"
# 相对路径的起点即当前py文件所在的目录
# mode为 r 或者r+ 的时候,路径对应的文件必须存在,否则报错
# mode为 w w+ a a+ 的时候,路径对应的文件可以不存在,不存在的时候会自动创建
file_path = "./test_file.txt"
# 因为我们在编写文件的时候,使用的就是UTF-8编码,所以我们读取的时候,也要指定编码为 UTF-8
# 读取文件内容
file_test = open(file_path, mode="r", encoding="UTF-8")
# <class '_io.TextIOWrapper'>
print(type(file_test))
# 从当前指针所在位置开始,将后面的所有的数据都读取到一个列表中
# 每一个元素表示文件中的一行内容
content_list = file_test.readlines()
# print(content_list)
# for 循环遍历
for line in content_list:
    print(line, end="")

print()
# close() 方法用于关闭一个已打开的文件。关闭后的文件不能再进行读写操作, 否则会触发 ValueError 错误。 close() 方法允许调用多次。
# 当 file 对象,被引用到操作另外一个文件时,Python 会自动关闭之前的 file 对象。 使用 close() 方法关闭文件是一个好的习惯。
# close在最终关闭文件对象之前,会自动调用flush方法
# 文件对象最后需要关闭
file_test.close()

print("-----------")
# 其实可以直接将文件对象拿来循环,感觉是自动调用了文件对象的readlines方法
# 但是这有一个问题啊,怎么关闭文件对象呢?
# 此时 文件对象未关闭
for line in open(file_path, mode="r", encoding="UTF-8"):
    print(line, end="")

print()
print("-----------")

输出:

<class '_io.TextIOWrapper'>
11111111111
22222222222222
33333333333
AAAAAAAAAAAAAAAA
BBBBBBBBBBBBBBBBB
CCCCCCCCCCCCCCCC
卧槽,真牛逼,真的假的
-----------
11111111111
22222222222222
33333333333
AAAAAAAAAAAAAAAA
BBBBBBBBBBBBBBBBB
CCCCCCCCCCCCCCCC
卧槽,真牛逼,真的假的
-----------

通过 with open 语法,可以自动帮我们关闭文件对象,防止我们忘记

关于 with 关键字,请看《错误和异常处理.md》中的 预定义的finally 小节

# with open 语法,可以自动帮我们关闭文件对象,防止我们忘记
with open(file_path, mode="r", encoding="UTF-8") as file_handler:
    content = file_handler.read()
    print(content, type(content))

print("-----------")

输出:

11111111111
22222222222222
33333333333
AAAAAAAAAAAAAAAA
BBBBBBBBBBBBBBBBB
CCCCCCCCCCCCCCCC
卧槽,真牛逼,真的假的 <class 'str'>
-----------

如果文件比较大,我们可以使用生成器读取文件内容,这样,就不用将文件中的所有内容读到内存中,占用内存

def file_iterator(file_obj):
    while True:
        # readline() 方法用于从文件当前指针所在位置开始读取整行,包括 换行符(可以用字符串的strip方法去除)。如果指定了一个非负数的参数,则返回指定大小的字节数,其中换行符也算字节数
        # 已经读取了最后一行之后,在调用此方法,会返回空字符串,并且一直调用,一直返回空字符串
        line_str = file_obj.readline()
        # 空字符串 '' ,作为条件表达式,会被bool()转化为 Boolean,结果为 False
        if line_str:
            yield line_str
        else:
            # 抛出 StopIteration
            return


file_obj = open(file_path, mode="r", encoding="UTF-8")
line_reader = file_iterator(file_obj)
print(next(line_reader))
print(next(line_reader))

for line in line_reader:
    print(line, end="")

print()
file_obj.close()

print("--------------------------------")

输出:

11111111111

22222222222222

33333333333
AAAAAAAAAAAAAAAA
BBBBBBBBBBBBBBBBB
CCCCCCCCCCCCCCCC
卧槽,真牛逼,真的假的
--------------------------------

查看文件当前的指针所在的位置

# 查看文件读取到哪里了
# 读写文件的时候,有一个指针标识我们当前在哪里,所有的读写操作都是从当前位置开始的
# tell() 方法返回文件的当前位置,即文件指针当前位置, 它是从文件开头开始算起的字节数。
# 常见字符的字节数
# 一个数字字符算1个字节,
# 一个字母字符算1个字节,
# 一个换行符算两个字节,在Windows中占两个字节,在Linux中占一个字节
# 一个中文字符算3个字节,这个跟Java不一样,Java中一个中文字符占用2个字节
#
# 文件内容为 Python真棒
file_path_char = "./char_test.txt"
char_file = open(file_path_char, mode="r", encoding="UTF-8")
print(char_file.readline())
# 整个文件就一行, 调用一次 readline就读完了, 所以当前的位置可以算出来, 6*6 + 2*3 12个字节
print(char_file.tell())
# 在不带b的模式下,可以使用seek() 方法,但是 offset 参数必须是 0
# 回到开头,可以继续读第一行
char_file.seek(0, 0)
# read() 方法用于从文件中当前指针所在位置开始读取指定的字符数(文本模式 t)或字节数(二进制模式 b),如果未给定参数 size 或 size 为负数则读取文件所有内容到一个字符串中。size默认为 -1
# 输出从当前指针开始的6的字符,也就是输出 Python
print(char_file.read(6))
char_file.seek(0, 1)
print(char_file.readline())
char_file.seek(0, 2)
print(char_file.readline())

print("--------------------------------")

输出:

Python真棒
12
Python
真棒

--------------------------------

移动移动文件当前的指针所在的位置

通过移动文件指针的位置,我们可以从文件的任意位置读取任意个数的字节,但是要注意,(在使用二进制模式的时候)要将字节解码为中文的时候,很有可能因为会因为字节不完整,导致无法解码,比如真棒这两个字总共 6 个字节,结果字节对象里只有 5 个字节,那这样进行解码的时候就会报错

为了不报错,我们可以指定 errors 参数 decode(encoding='utf-8', errors='ignore'),这样,当无法解码的时候,就不会报错。

本质上来说,从任意位置读取字节,然后将其转化为中文字符,这个操作还是比较有难度和风险的

# seek() 方法用于移动文件读取指针到指定位置。
# f.seek(offset, from_what) 函数。
# offset 需要移动偏移的字节数,表示字符个数,正数表示向后移动,负数表示向前移动
# from_what 表示相对位置, 如果是 0 表示开头, 如果是 1 表示当前位置, 2 表示文件的结尾,
# 例如:
# seek(x,0) :从起始位置即文件首行首字符开始向后移动 x 个字节
# seek(x,1) :表示从当前位置往后移动x个字节
# seek(-x,2):表示从文件的结尾往前移动x个字节
#
# 文件内容为 Python真棒
file_path_binary_char = "./char_binary_test.txt"
char_file = open(file_path_binary_char, mode="rb")
# rb 模式下,readline方法读出来的是字节(但是数字字符和英文字符还是原来的样子,在utf-8或者unicode编码中,字母和数字兼容 ASCII 编码,所以没有编码,编码就是自身),想要展示为字符串,需要解码
# b'Python\xe7\x9c\x9f\xe6\xa3\x92'
print(char_file.readline())
# 输出 12
print(char_file.tell())
# 回到开头
char_file.seek(0, 0)
# 然后移动到倒数 2个中文字符,和3个英文字符,也就是9个字节
char_file.seek(-9, 2)
# 再向后移动3个英文字符,也就是3个字节
char_file.seek(3, 1)
# read() 方法用于从文件读取指定的字符数(文本模式 t)或字节数(二进制模式 b),如果未给定参数 size 或 size 为负数则读取文件所有内容。默认为 -1
# 输出从当前指针开始的6个字节,也就是两个中文字符,输出 真棒 对应的字节
binary_read = char_file.read(6)
# b'\xe7\x9c\x9f\xe6\xa3\x92' <class 'bytes'>
print(binary_read, type(binary_read))
# 输出 编码为中文:真棒
print("编码为中文:" + binary_read.decode('utf-8', ))
# 在使用带b的模式的时候,要将字节解码为中文的时候,很有可能因为会因为字节不完整,导致无法解码,比如真棒这两个字总共6个字节,结果字节对象里只有5个字节,那这样进行解码的时候就会报错
# 为了不报错,我们可以指定errors参数 decode(encoding='utf-8', errors='ignore'),这样,当无法解码的时候,就不会报错
print("--------------------------------")

输出:

b'Python\xe7\x9c\x9f\xe6\xa3\x92'
12
b'\xe7\x9c\x9f\xe6\xa3\x92' <class 'bytes'>
编码为中文:真棒
--------------------------------

边读边改

TODO

写操作

open 方法的 mode 选择以 w 或者 a 开头的即可

常用方法

很奇怪的是,Python 并没有自动在内容后面加上换行的方法,可能是为了兼容二进制模式

我们调用 write 方法,内容并未真正直接写入文件,而是会积攒在程序的内存中,称之为缓冲区,当调用 flush 的时候,内容会真正写入文件,为什么这么做,这样做是避免频繁的操作硬盘,导致效率下降(攒一堆,一次性写磁盘)

实践

简单实践

这里我们演示写入,其实追加写也很简单,将模式改为以 a 开头即可

# mode为 r 或者r+ 的时候,路径对应的文件必须存在,否则报错
# mode为 w w+ a a+ 的时候,路径对应的文件可以不存在,不存在的时候会自动创建
file_path_write = "./write_test.txt"
file_write = open(file_path_write, mode="w", encoding="UTF-8")
for i in range(0, 10):
    # write() 方法用于向文件中写入指定字符串。
    # 在文件关闭前或缓冲区刷新前,字符串内容存储在缓冲区中,这时你在文件中是看不到写入的内容的。
    # 如果文件打开模式带 b,那写入文件内容时,str (参数)要用 encode 方法转为 bytes 形式,否则报错:TypeError: a bytes-like object is required, not 'str'。
    file_write.write(str(i) + "\n")

line_list = ["批量插入1\n", "批量插入2\n", "批量插入3\n", "批量插入4\n"]
# writelines() 方法用于向文件中写入一序列的字符串。
# 这一序列字符串可以是由迭代对象产生的,如一个字符串列表。
# 换行需要制定换行符 \n。
file_write.writelines(line_list)

# flush() 方法是用来刷新缓冲区的,即将缓冲区中的数据立刻写入文件,同时清空缓冲区,不需要是被动的等待输出缓冲区写入。
# 一般情况下,文件关闭后会自动刷新缓冲区,但有时你需要在关闭前刷新它,这时就可以使用 flush() 方法。
file_write.flush()

file_write.close()

写入的文件

0
1
2
3
4
5
6
7
8
9
批量插入1
批量插入2
批量插入3
批量插入4

缓冲区配置实践

对缓冲区的配置,请看 open 小节的 buffering 参数,

Python 的文件操作自带缓存,确守比 Java 方便

行缓冲

意思是,只缓冲一行,最终的效果就是,(首先所有的数据都会先写入缓冲区)一旦缓冲区中的字符出现一个换行,缓冲区中的数据刷入文件中。所以当我们使用 write() 方法写入文件的时候,只要写入的内容中包含换行符 \n,就相当于在 write() 方法后调用了一次 flush() 方法

file_path_write_buffer = "./write_buffer_test.txt"
# buffering=1 行缓存
# 如果模式为a,则会一直追加
file_write_buffer = open(file_path_write_buffer, mode="a", encoding="UTF-8", buffering=1)
# 使用write()方法写入文件的时候,只要写入的内容中包含换行符`\n`,就相当于在write()方法后调用了一次flush()方法
file_write_buffer.write("1111")
file_write_buffer.write("222\n")
file_write_buffer.write("333")
file_write_buffer.write("444\n")
file_write_buffer.write("55555")
file_write_buffer.write("666\n777777")
file_write_buffer.write("88888")
file_write_buffer.flush()
file_write_buffer.close()

效果就是,

写完 222\n,刷新缓冲区,文件中添加 1111222\n

写完 444\n,刷新缓冲区,文件中添加一行 333444\n

以此类推,到写完 666\n777777 的时候,777777 也会被刷新到磁盘文件中。

指定大小的缓冲区

指定大小的缓冲区,仅在二进制模式下能生效,文本模式下不生效,很好用

file_path_write_buffer_2 = "./write_buffer_test_2.txt"
# buffering > 1 的缓存设置
# 仅在二进制模式下能生效,文本模式下不生效,虽然官方文档没说,但是我测试出来是这样的
file_write_buffer_2 = open(file_path_write_buffer_2, mode="wb", buffering=3)
# 超过3个字节的输入,都会直接写入到物理文件中
file_write_buffer_2.write("我靠".encode("UTF-8"))
file_write_buffer_2.write("8888".encode("UTF-8"))
file_write_buffer_2.write("111\n".encode("UTF-8"))
file_write_buffer_2.flush()
file_write_buffer_2.close()

简单应用实践

复制文本文件

# 文本文件复制
file_source = open("./copy_source.csv", "r", encoding="utf-8")
file_target = open("./copy_target.csv", "w", encoding="utf-8")
file_target.write("name,age,score,type\n")
for line in file_source:
    # 去掉两端的空格,最主要的是去掉最后的换行符
    line = line.strip()
    line_parts = line.split(",")
    if len(line_parts) < 4 or (not str(line_parts[2]).isdigit()):
        continue
    if int(line_parts[2]) >= 100 and line_parts[3] == 'prod':
        file_target.write(line + "\n")

file_source.close()
file_target.close()

复制二进制文件 比如视频

# 二进制复制,比如视频
# 这里就不把视频上传到GitHub了,太大了
file_binary_source = open("F:/羽毛球打球视频/2022年09月30日/VID_20220930_184501.mp4", "rb")
file_binary_target = open("F:/羽毛球打球视频/2022年09月30日/VID_20220930_184501_复制.mp4", "wb", )

while True:
    # 一次读取2kb
    content_binary = file_binary_source.read(2048)
    # 空字节 b'' ,作为条件表达式,会被bool()转化为 Boolean,结果为 False
    if content_binary:
        file_binary_target.write(content_binary)
    else:
        break

file_binary_source.close()
file_binary_target.close()